package com.bincloud.vue.admin.web.service.api.impl;

import com.bincloud.vue.admin.common.constanst.RedisConstant;
import com.bincloud.vue.admin.common.exception.BaseException;
import com.bincloud.vue.admin.common.exception.CodeMsg;
import com.bincloud.vue.admin.common.util.BaseEntityUtils;
import com.bincloud.vue.admin.common.util.IdUtils;
import com.bincloud.vue.admin.common.util.ObjectUtils;
import com.bincloud.vue.admin.common.util.StringUtils;
import com.bincloud.vue.admin.model.entity.UmsUserLoginLog;
import com.bincloud.vue.admin.web.dto.api.auth.AuthLoginReqDTO;
import com.bincloud.vue.admin.web.dto.api.auth.AuthResDTO;
import com.bincloud.vue.admin.web.dto.api.auth.AuthValidateReqDTO;
import com.bincloud.vue.admin.web.service.api.AuthService;
import com.bincloud.vue.admin.web.service.ums.IUmsUserLoginLogService;
import com.bincloud.vue.admin.web.service.ums.IUmsUserService;
import com.bincloud.vue.admin.web.util.config.UmsUserConfig;
import com.bincloud.vue.admin.web.util.jasypt.JasyptService;
import com.bincloud.vue.admin.web.util.redis.RedisService;
import com.bincloud.vue.admin.web.util.request.RequestService;
import com.bincloud.vue.admin.web.util.security.VueAdminUserDetailsService;
import com.bincloud.vue.admin.web.util.token.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @Description: 用户接口实现
 * @Author: bincloud, mvpzhou
 * @Date: Create By bincloud, mvpzhou on 2022/2/16
 */
@Service
@Slf4j
public class AuthServiceImpl implements AuthService {
    @Resource
    private IUmsUserService umsUserService;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private RedisService redisService;
    @Resource
    private VueAdminUserDetailsService userDetailsService;
    @Resource
    private UmsUserConfig userConfig;
    @Resource
    private JasyptService jasyptService;
    @Resource
    private IUmsUserLoginLogService userLoginLogService;
    @Resource
    private RequestService requestService;
    @Resource
    private TokenService tokenService;

    /**
     * 用户认证 一次验证
     * 1.账号
     * 2.密码
     * 3.验证码（图形）
     * 规则
     * 1.密码错误连续N次锁定用户
     * 2.用户锁定时长
     *
     * @param reqDTO 入参
     * @return 返参
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AuthResDTO loginUser(AuthLoginReqDTO reqDTO) {
        try {
            // 验证验证码
            this.validateCaptcha(reqDTO.getUsername(), reqDTO.getCaptcha());
            // 验证黑名单
            this.validateAccount(reqDTO.getUsername());
            // 获取用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(reqDTO.getUsername());
            // 验证密码
            this.validatePassword(reqDTO.getUsername(), reqDTO.getPassword(), userDetails.getPassword());
            // 更新最后登录时间
            umsUserService.updateLastLogin(reqDTO.getUsername());
            // 新增登录记录
            this.saveLoginLog(reqDTO.getUsername());
            // 生成凭证
            String token = tokenService.generateToken(userDetails);
            // 组装返参
            AuthResDTO resDTO = new AuthResDTO();
            resDTO.setToken(token);
            return resDTO;
        } catch (BaseException e) {
            throw new BaseException(e.getMessage());
        } catch (Exception e) {
            throw new BaseException(CodeMsg.FAIL);
        }
    }

    /**
     * 用户认证 二次验证
     * 1.账号
     * 2.密码
     * 3.验证码（图形）
     * 规则
     * 1.密码错误连续N次锁定用户
     * 2.用户锁定时长
     * 3.缓存临时用户
     *
     * @param reqDTO 入参
     * @return 返参
     */
    @Override
    public AuthResDTO twoFactorLogin(AuthLoginReqDTO reqDTO) {
        try {
            // 验证验证码
            this.validateCaptcha(reqDTO.getUsername(), reqDTO.getCaptcha());
            // 验证黑名单
            this.validateAccount(reqDTO.getUsername());
            // 获取用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(reqDTO.getUsername());
            // 验证密码
            this.validatePassword(reqDTO.getUsername(), reqDTO.getPassword(), userDetails.getPassword());
            // 缓存临时用户
            String key = RedisConstant.TEMPORARY_USER_CACHE + reqDTO.getUsername();
            String token = IdUtils.simpleUUID();
            redisService.set(key, token, userConfig.getPasswordLockTime(), TimeUnit.MINUTES);
            // 组装返参
            AuthResDTO resDTO = new AuthResDTO();
            resDTO.setToken(token);
            return resDTO;
        } catch (BaseException e) {
            throw new BaseException(e.getMessage());
        } catch (Exception e) {
            throw new BaseException(CodeMsg.FAIL);
        }
    }

    /**
     * 用户认证 二次验证
     * 1.账号
     * 2.密码
     * 3.验证码（图形）
     * 规则
     * 1.密码错误连续N次锁定用户
     * 2.用户锁定时长
     * 3.缓存临时用户
     *
     * @param reqDTO 入参
     * @return 返参
     */
    @Override
    public AuthResDTO validateLogin(AuthValidateReqDTO reqDTO) {
        try {
            // 验证验证码
            this.validateTwoFactorCaptcha(reqDTO.getUsername(), reqDTO.getCaptcha());
            // 获取用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(reqDTO.getUsername());
            // 更新最后登录时间
            umsUserService.updateLastLogin(reqDTO.getUsername());
            // 新增登录记录
            this.saveLoginLog(reqDTO.getUsername());
            // 生成凭证
            String token = tokenService.generateToken(userDetails);
            // 组装返参
            AuthResDTO resDTO = new AuthResDTO();
            resDTO.setToken(token);
            return resDTO;
        } catch (BaseException e) {
            throw new BaseException(e.getMessage());
        } catch (Exception e) {
            throw new BaseException(CodeMsg.FAIL);
        }
    }

    private void validateTwoFactorCaptcha(String username, String captcha) {
        if (StringUtils.isBlank(username) || StringUtils.isBlank(captcha)) {
            throw new BaseException(CodeMsg.FAIL);
        }
        // 组装key
        String key = RedisConstant.TWO_FACTOR_CAPTCHA + username;
        // 获取value
        Object temp = redisService.get(key);
        if (ObjectUtils.isEmpty(temp)) {
            throw new BaseException(CodeMsg.CAPTCHA_TWO_FACTOR_EXPIRED);
        }
        String value = (String) temp;
        // 删除缓存
        if (StringUtils.isNotBlank(value)) {
            redisService.del(key);
        }
        // 1 验证码过期
        if (StringUtils.isBlank(value)) {
            throw new BaseException(CodeMsg.CAPTCHA_TWO_FACTOR_EXPIRED);
        }
        // 2 验证码错误
        if (StringUtils.isNotEquals(value, captcha)) {
            throw new BaseException(CodeMsg.CAPTCHA_TWO_FACTOR_ERROR);
        }
        // 删除临时用户缓存
        String tempKey = RedisConstant.TEMPORARY_USER_CACHE + username;
        redisService.del(tempKey);
    }

    private void saveLoginLog(String username) {
        UmsUserLoginLog logEntity = new UmsUserLoginLog();
        logEntity.setUsername(username).setIp(requestService.getIpAddress());
        BaseEntityUtils.create(logEntity, username);
        userLoginLogService.save(logEntity);
    }

    private void validatePassword(String username, String password, String enPassword) {
        if (StringUtils.isBlank(username) || StringUtils.isBlank(password) || StringUtils.isBlank(enPassword)) {
            throw new BaseException(CodeMsg.FAIL);
        }
        // RSA 解密
        String dePassword = jasyptService.decrypt(password);
        // 对比密码
        if (!passwordEncoder.matches(dePassword, enPassword)) {
            // 获取错误次数 超过次数进行锁定
            String lockKey = RedisConstant.BAD_PASSWORD_CACHE + username;
            Integer errorTimes = (Integer) redisService.get(lockKey);
            if (ObjectUtils.isNull((errorTimes))) {
                // 首次错误
                errorTimes = 1;
            } else {
                // 非首次错误
                errorTimes+=1;
                // 超过次数 进入登录黑名单
                if (errorTimes >= userConfig.getPasswordMaxRetryCount()) {
                    String blackKey = RedisConstant.USER_LOGIN_BLACKLIST + username;
                    redisService.set(blackKey, errorTimes, userConfig.getPasswordLockTime(), TimeUnit.MINUTES);
                }
            }
            // N 分钟内错误密码缓存次数
            redisService.set(lockKey, errorTimes, userConfig.getPasswordLockTime(), TimeUnit.MINUTES);
            // 抛出异常
            throw new BaseException(CodeMsg.ACCOUNT_PASSWORD_VALIDATE_ERROR);
        }
        // 删除错误次数
        String lockKey = RedisConstant.BAD_PASSWORD_CACHE + username;
        Integer errorTimes = (Integer) redisService.get(lockKey);
        if (ObjectUtils.isNotNull(errorTimes)) {
            redisService.del(lockKey);
        }
    }

    private void validateAccount(String username) {
        if (StringUtils.isBlank(username)) {
            throw new BaseException(CodeMsg.FAIL);
        }
        String blackKey = RedisConstant.USER_LOGIN_BLACKLIST + username;
        Integer blackValue = (Integer) redisService.get(blackKey);
        if (ObjectUtils.isNotNull(blackValue)) {
            Long expire = redisService.getExpire(blackKey, TimeUnit.MINUTES);
            throw new BaseException(CodeMsg.ACCOUNT_LOCKED.getCode(), CodeMsg.ACCOUNT_LOCKED.getMsg() + ", 请在" + expire + "分钟后重试");
        }
    }

    private void validateCaptcha(String username, String captcha) {
        if (StringUtils.isBlank(username) || StringUtils.isBlank(captcha)) {
            throw new BaseException(CodeMsg.FAIL);
        }
        // 组装key
        String key = RedisConstant.CAPTCHA + username;
        // 获取value
        Object temp = redisService.get(key);
        if (ObjectUtils.isEmpty(temp)) {
            throw new BaseException(CodeMsg.CAPTCHA_EXPIRED);
        }
        String value = (String) temp;
        // 删除缓存
        if (StringUtils.isNotBlank(value)) {
            redisService.del(key);
        }
        // 1 验证码过期
        if (StringUtils.isBlank(value)) {
            throw new BaseException(CodeMsg.CAPTCHA_EXPIRED);
        }
        // 2 验证码错误
        if (StringUtils.isNotEquals(value, captcha)) {
            throw new BaseException(CodeMsg.CAPTCHA_ERROR);
        }
    }


}
